home *** CD-ROM | disk | FTP | other *** search
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """Simplified Twisted Deferreds."""
- # Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License along
- # with this program; if not, write to the Free Software Foundation, Inc.,
- # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
- __author__ = "Sebastian Heinlein <devel@glatzor.de>"
-
- from functools import wraps
- import sys
-
- import dbus
-
- class AlreadyCalledDeferred(Exception):
- """The Deferred is already running a callback."""
-
-
- class DeferredException(object):
- """Allows to defer exceptions."""
-
- def __init__(self, type=None, value=None, traceback=None):
- """Return a new DeferredException instance.
-
- If type, value and traceback are not specified the infotmation
- will be retreieved from the last caught exception:
-
- >>> try:
- ... raise Exception("Test")
- ... except:
- ... deferred_exc = DeferredException()
- >>> deferred_exc.raise_exception()
- Traceback (most recent call last):
- ...
- Exception: Test
-
- Alternatively you can set the exception manually:
-
- >>> exception = Exception("Test 2")
- >>> deferred_exc = DeferredException(exception)
- >>> deferred_exc.raise_exception()
- Traceback (most recent call last):
- ...
- Exception: Test 2
- """
- self.type = type
- self.value = value
- self.traceback = traceback
- if isinstance(type, Exception):
- self.type = type.__class__
- self.value = type
- elif not type or not value:
- self.type, self.value, self.traceback = sys.exc_info()
-
- def raise_exception(self):
- """Raise the stored exception."""
- raise self.type, self.value, self.traceback
-
- def catch(self, *errors):
- """Check if the stored exception is a subclass of one of the
- provided exception classes. If this is the case return the
- matching exception class. Otherwise raise the stored exception.
-
- >>> exc = DeferredException(SystemError())
- >>> exc.catch(Exception) # Will catch the exception and return it
- <type 'exceptions.Exception'>
- >>> exc.catch(OSError) # Won't catch and raise the stored exception
- Traceback (most recent call last):
- ...
- SystemError
-
- This method can be used in errbacks of a Deferred:
-
- >>> def dummy_errback(deferred_exception):
- ... '''Error handler for OSError'''
- ... deferred_exception.catch(OSError)
- ... return "catched"
-
- The above errback can handle an OSError:
-
- >>> deferred = Deferred()
- >>> deferred.add_errback(dummy_errback)
- >>> deferred.errback(OSError())
- >>> deferred.result
- 'catched'
-
- But fails to handle a SystemError:
-
- >>> deferred2 = Deferred()
- >>> deferred2.add_errback(dummy_errback)
- >>> deferred2.errback(SystemError())
- >>> deferred2.result #doctest: +ELLIPSIS
- <aptdaemon.defer.DeferredException object at 0x...>
- >>> deferred2.result.value
- SystemError()
- """
- for err in errors:
- if issubclass(self.type, err):
- return err
- self.raise_exception()
-
-
- class Deferred(object):
- """The Deferred allows to chain callbacks.
-
- There are two type of callbacks: normal callbacks and errbacks, which
- handle an exception in a normal callback.
-
- The callbacks are processed in pairs consisting of a normal callback
- and an errback. A normal callback will return its result to the
- callback of the next pair. If an exception occurs, it will be handled
- by the errback of the next pair. If an errback doesn't raise an error
- again, the callback of the next pair will be called with the return
- value of the errback. Otherwise the exception of the errback will be
- returned to the errback of the next pair.
-
- CALLBACK1 ERRBACK1
- | \ / |
- result failure result failure
- | \ / |
- | \ / |
- | X |
- | / \ |
- | / \ |
- | / \ |
- CALLBACK2 ERRBACK2
- | \ / |
- result failure result failure
- | \ / |
- | \ / |
- | X |
- | / \ |
- | / \ |
- | / \ |
- CALLBACK3 ERRBACK3
- """
-
- def __init__(self):
- """Return a new Deferred instance."""
- self.callbacks = []
- self.errbacks = []
- self.called = False
- self.paused = False
- self._running = False
-
- def add_callbacks(self, callback, errback=None,
- callback_args=None, callback_kwargs=None,
- errback_args=None, errback_kwargs=None):
- """Add a pair of callables (function or method) to the callback and
- errback chain.
-
- Keyword arguments:
- callback -- the next chained challback
- errback -- the next chained errback
- callback_args -- list of additional arguments for the callback
- callback_kwargs -- dict of additional arguments for the callback
- errback_args -- list of additional arguments for the errback
- errback_kwargs -- dict of additional arguments for the errback
-
- In the following example the first callback pairs raises an
- exception that is catched by the errback of the second one and
- processed by the third one.
-
- >>> def callback(previous):
- ... '''Return the previous result.'''
- ... return "Got: %s" % previous
- >>> def callback_raise(previous):
- ... '''Fail and raise an exception.'''
- ... raise Exception("Test")
- >>> def errback(error):
- ... '''Recover from an exception.'''
- ... #error.catch(Exception)
- ... return "catched"
- >>> deferred = Deferred()
- >>> deferred.callback("start")
- >>> deferred.result
- 'start'
- >>> deferred.add_callbacks(callback_raise, errback)
- >>> deferred.result #doctest: +ELLIPSIS
- <aptdaemon.defer.DeferredException object at 0x...>
- >>> deferred.add_callbacks(callback, errback)
- >>> deferred.result
- 'catched'
- >>> deferred.add_callbacks(callback, errback)
- >>> deferred.result
- 'Got: catched'
- """
- assert callable(callback)
- assert errback is None or callable(errback)
- if errback is None:
- errback = _passthrough
- self.callbacks.append(((callback,
- callback_args or ([]),
- callback_kwargs or ({})),
- (errback or (_passthrough),
- errback_args or ([]),
- errback_kwargs or ({}))))
- if self.called:
- self._next()
-
- def add_errback(self, func, *args, **kwargs):
- """Add a callable (function or method) to the errback chain only.
-
- If there isn't any exception the result will be passed through to
- the callback of the next pair.
-
- The first argument is the callable instance followed by any
- additional argument that will be passed to the errback.
-
- The errback method will get the most recent DeferredException and
- and any additional arguments that was specified in add_errback.
-
- If the errback can catch the exception it can return a value that
- will be passed to the next callback in the chain. Otherwise the
- errback chain will not be processed anymore.
-
- See the documentation of defer.DeferredException.catch for
- further information.
-
- >>> def catch_error(deferred_error, ignore=False):
- ... if ignore:
- ... return "ignored"
- ... deferred_error.catch(Exception)
- ... return "catched"
- >>> deferred = Deferred()
- >>> deferred.errback(SystemError())
- >>> deferred.add_errback(catch_error, ignore=True)
- >>> deferred.result
- 'ignored'
- """
- self.add_callbacks(_passthrough, func, errback_args=args,
- errback_kwargs=kwargs)
-
- def add_callback(self, func, *args, **kwargs):
- """Add a callable (function or method) to the callback chain only.
-
- An error would be passed through to the next errback.
-
- The first argument is the callable instance followed by any
- additional argument that will be passed to the callback.
-
- The callback method will get the result of the previous callback
- and any additional arguments that was specified in add_callback.
-
- >>> def callback(previous, counter=False):
- ... if counter:
- ... return previous + 1
- ... return previous
- >>> deferred = Deferred()
- >>> deferred.add_callback(callback, counter=True)
- >>> deferred.callback(1)
- >>> deferred.result
- 2
- """
- self.add_callbacks(func, _passthrough, callback_args=args,
- callback_kwargs=kwargs)
-
- def errback(self, error=None):
- """Start processing the errorback chain starting with the
- provided exception or DeferredException.
-
- If an exception is specified it will be wrapped into a
- DeferredException. It will be send to the first errback or stored
- as finally result if not any further errback has been specified yet.
-
- >>> deferred = Deferred()
- >>> deferred.errback(Exception("Test Error"))
- >>> deferred.result #doctest: +ELLIPSIS
- <aptdaemon.defer.DeferredException object at 0x...>
- >>> deferred.result.raise_exception()
- Traceback (most recent call last):
- ...
- Exception: Test Error
- """
- if self.called:
- raise AlreadyCalledDeferred()
- if not isinstance(error, DeferredException):
- assert isinstance(error, Exception)
- error = DeferredException(error.__class__, error, None)
- self.called = True
- self.result = error
- self._next()
-
- def callback(self, result=None):
- """Start processing the callback chain starting with the
- provided result.
-
- It will be send to the first callback or stored as finally
- one if not any further callback has been specified yet.
-
- >>> deferred = Deferred()
- >>> deferred.callback("done")
- >>> deferred.result
- 'done'
- """
- if self.called:
- raise AlreadyCalledDeferred()
- self.called = True
- self.result = result
- self._next()
-
- def _continue(self, result):
- """Continue processing the Deferred with the given result."""
- self.result = result
- self.paused = False
- if self.called:
- self._next()
-
- def _next(self):
- """Process the next callback."""
- if self._running or self.paused:
- return
- while self.callbacks:
- # Get the next callback pair
- next_pair = self.callbacks.pop(0)
- # Continue with the errback if the last result was an exception
- callback, args, kwargs = next_pair[isinstance(self.result,
- DeferredException)]
- try:
- self.result = callback(self.result, *args, **kwargs)
- except:
- self.result = DeferredException()
- finally:
- self._running = False
- if isinstance(self.result, Deferred):
- # If a Deferred was returned add this deferred as callbacks to
- # the returned one. As a result the processing of this Deferred
- # will be paused until all callbacks of the returned Deferred
- # have been performed
- self.result.add_callbacks(self._continue, self._continue)
- self.paused == True
- break
-
- if isinstance(self.result, DeferredException):
- # Print the exception to stderr and stop if there aren't any
- # further errbacks to process
- sys.excepthook(self.result.type, self.result.value,
- self.result.traceback)
- return False
-
- def defer(func, *args, **kwargs):
- """Invoke the given function that may or not may be a Deferred.
-
- If the return object of the function call is a Deferred return, it.
- Otherwise wrap it into a Deferred.
-
- >>> defer(lambda x: x, 10) #doctest: +ELLIPSIS
- <aptdaemon.defer.Deferred object at 0x...>
-
- >>> deferred = defer(lambda x: x, "done")
- >>> deferred.result
- 'done'
-
- >>> deferred = Deferred()
- >>> defer(lambda: deferred) == deferred
- True
- """
- assert callable(func)
- try:
- result = func(*args, **kwargs)
- except:
- result = DeferredException()
- if isinstance(result, Deferred):
- return result
- deferred = Deferred()
- deferred.callback(result)
- return deferred
-
-
- def dbus_deferred_method(*args, **kwargs):
- """Export the decorated method on the D-Bus and handle a maybe
- returned Deferred.
-
- This decorator can be applied to methods in the same way as the
- @dbus.service.method method, but it correctly handles the case where
- the method returns a Deferred.
-
- This decorator was kindly taken from James Henstridge blog post and
- adopted:
- http://blogs.gnome.org/jamesh/2009/07/06/watching-iview-with-rygel/
- """
- def decorator(function):
- function = dbus.service.method(*args, **kwargs)(function)
- @wraps(function)
- def wrapper(*args, **kwargs):
- def ignore_none_callback(*cb_args):
- # The deferred method at least returns an tuple containing
- # only None. Ignore this case.
- if cb_args == (None,):
- dbus_callback()
- else:
- dbus_callback(*cb_args)
- dbus_callback = kwargs.pop('_dbus_callback')
- dbus_errback = kwargs.pop('_dbus_errback')
- deferred = defer(function, *args, **kwargs)
- deferred.add_callback(ignore_none_callback)
- deferred.add_errback(lambda error: dbus_errback(error.value))
- # The @wraps decorator has copied over the attributes added by
- # the @dbus.service.method decorator, but we need to manually
- # set the async callback attributes.
- wrapper._dbus_async_callbacks = ('_dbus_callback', '_dbus_errback')
- return wrapper
- return decorator
-
- def _passthrough(arg):
- return arg
-
- # vim:tw=4:sw=4:et
-